﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Aeon.Emulator;
using System.IO;
using Aeon.Emulator.Dos.VirtualFileSystem;

namespace Aeon.Configuration
{
    /// <summary>
    /// Describes the configurable settings on an emulated system.
    /// </summary>
    public sealed class VirtualMachineConfiguration : AeonConfiguration
    {
        #region Private Fields
        private string initialDirectory;
        private string launchTarget;
        private bool absoluteMouse;
        private bool hideUI;
        private int emulationSpeed = 90;
        private string title;
        private readonly MountedDriveCollection drives = new MountedDriveCollection();
        private readonly ObservableCollection<IDeviceConfiguration> devices = new ObservableCollection<IDeviceConfiguration>();
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the VirtualMachineConfiguration class.
        /// </summary>
        public VirtualMachineConfiguration()
        {
            this.drives.CollectionChanged += (s, e) => ValidateStartupPath();
            ValidateStartupPath();
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets or sets the startup directory.
        /// </summary>
        public string StartupPath
        {
            get
            {
                return this.initialDirectory;
            }
            set
            {
                if(value != this.initialDirectory)
                {
                    this.initialDirectory = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("StartupPath"));
                    ValidateStartupPath();
                }
            }
        }
        /// <summary>
        /// Gets or sets the program to launch on startup. This may be null.
        /// </summary>
        public string Launch
        {
            get
            {
                return this.launchTarget;
            }
            set
            {
                if(value != this.launchTarget)
                {
                    this.launchTarget = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("Launch"));
                    ValidateLaunch();
                }
            }
        }
        /// <summary>
        /// Gets or sets a value indicating whether the mouse should be in non-capturing mode.
        /// </summary>
        public bool IsMouseAbsolute
        {
            get
            {
                return this.absoluteMouse;
            }
            set
            {
                if(value != this.absoluteMouse)
                {
                    this.absoluteMouse = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("IsMouseAbsolute"));
                }
            }
        }
        /// <summary>
        /// Gets or sets the emulation speed as a percentage from 10 to 100.
        /// </summary>
        public int EmulationSpeed
        {
            get
            {
                return this.emulationSpeed;
            }
            set
            {
                if(value != this.emulationSpeed)
                {
                    this.emulationSpeed = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("EmulationSpeed"));
                }
            }
        }
        /// <summary>
        /// Gets or sets a value indicating whether the emulator UI should be hidden.
        /// </summary>
        public bool HideUserInterface
        {
            get
            {
                return this.hideUI;
            }
            set
            {
                if(value != this.hideUI)
                {
                    this.hideUI = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("HideUserInterface"));
                }
            }
        }
        /// <summary>
        /// Gets or sets the title of the configuration.
        /// </summary>
        public string Title
        {
            get 
            {
                return this.title;
            }
            set
            {
                if(value != this.title)
                {
                    this.title = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("Title"));
                }
            }
        }
        /// <summary>
        /// Gets the collection of emulated drives.
        /// </summary>
        public MountedDriveCollection Drives
        {
            get { return this.drives; }
        }
        /// <summary>
        /// Gets the collection of emulated devices.
        /// </summary>
        public ObservableCollection<IDeviceConfiguration> Devices
        {
            get { return this.devices; }
        }
        #endregion

        #region Public Static Methods
        /// <summary>
        /// Returns a new VirtualMachineConfiguration read from a file.
        /// </summary>
        /// <param name="fileName">Name of the configuration file.</param>
        /// <param name="mapper">Current ConfigurationMapper instance.</param>
        /// <returns>VirtualMachineConfiguration read from the file.</returns>
        public static VirtualMachineConfiguration Load(string fileName, ConfigurationMapper mapper)
        {
            if(fileName == null)
                throw new ArgumentNullException("fileName");
            if(mapper == null)
                throw new ArgumentNullException("mapper");

            using(var xmlReader = XmlReader.Create(fileName, new XmlReaderSettings() { IgnoreComments = true, IgnoreProcessingInstructions = true, IgnoreWhitespace = true }))
            {
                xmlReader.MoveToContent();
                var config = new VirtualMachineConfiguration();
                config.Deserialize(xmlReader, mapper);
                return config;
            }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Returns a deep copy of the VirtualMachineConfiguration instance.
        /// </summary>
        /// <returns>Deep copy of the VirtualMachineConfiguration instance.</returns>
        public VirtualMachineConfiguration Clone()
        {
            var newConfig = new VirtualMachineConfiguration()
            {
                StartupPath = this.StartupPath,
                Launch = this.Launch,
                IsMouseAbsolute = this.IsMouseAbsolute,
                EmulationSpeed = this.EmulationSpeed
            };

            foreach(var drive in this.Drives)
                newConfig.Drives.Add(drive.Clone());

            foreach(var device in this.Devices)
                newConfig.Devices.Add(device.Clone());

            return newConfig;
        }
        /// <summary>
        /// Writes the configuration to a file.
        /// </summary>
        /// <param name="fileName">Name of file where configuration is to be written.</param>
        /// <param name="mapper">Current ConfigurationMapper instance.</param>
        public void Save(string fileName, ConfigurationMapper mapper)
        {
            if(fileName == null)
                throw new ArgumentNullException("fileName");
            if(mapper == null)
                throw new ArgumentNullException("mapper");

            using(var xmlWriter = XmlWriter.Create(fileName, new XmlWriterSettings() { Indent = true }))
            {
                Serialize(xmlWriter, mapper);
            }
        }
        #endregion

        #region Protected Methods
        /// <summary>
        /// Serializes the configuration to an XML element.
        /// </summary>
        /// <param name="xmlWriter">XmlWriter used to serialize the object.</param>
        protected override void Serialize(XmlWriter xmlWriter)
        {
            throw new NotSupportedException();
        }
        /// <summary>
        /// Deserializes the configuration from an XML element.
        /// </summary>
        /// <param name="xmlReader">XmlReader used to deserialize the object.</param>
        protected override void Deserialize(XmlReader xmlReader)
        {
            throw new NotSupportedException();
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Serializes the configuration to XML.
        /// </summary>
        /// <param name="xmlWriter">XmlWriter which will be used to write the configuration.</param>
        /// <param name="context">Current ConfigurationMapper instance.</param>
        private void Serialize(XmlWriter xmlWriter, ConfigurationMapper context)
        {
            xmlWriter.WriteStartElement("AeonVirtualMachineConfiguration");

            xmlWriter.WriteAttributeString("StartupPath", this.StartupPath);
            xmlWriter.WriteAttributeString("Launch", this.Launch);
            xmlWriter.WriteAttributeString("IsMouseAbsolute", this.IsMouseAbsolute.ToString());
            xmlWriter.WriteAttributeString("EmulationSpeed", this.EmulationSpeed.ToString());

            xmlWriter.WriteStartElement("VirtualDrives");
            SerializeDrives(xmlWriter, context);
            xmlWriter.WriteEndElement(); // VirtualDrives

            xmlWriter.WriteStartElement("VirtualDevices");
            SerializeDevices(xmlWriter, context);
            xmlWriter.WriteEndElement(); // VirtualDevices

            xmlWriter.WriteEndElement(); // AeonVirtualMachineConfiguration
        }
        /// <summary>
        /// Serializes the drive configuration to XML.
        /// </summary>
        /// <param name="xmlWriter">XmlWriter which will be used to write the configuration.</param>
        /// <param name="context">Current ConfigurationMapper instance.</param>
        private void SerializeDrives(XmlWriter xmlWriter, ConfigurationMapper context)
        {
            foreach(var drive in this.Drives)
            {
                xmlWriter.WriteStartElement("VirtualDrive");

                xmlWriter.WriteAttributeString("DriveLetter", drive.DriveLetter.ToString());
                xmlWriter.WriteAttributeString("DriveType", drive.DriveType.ToString());
                if(drive.DriveConfiguration != null)
                {
                    var driveConfigInfo = context.DriveConfigurations.Where(d => d.ConfigType == drive.DriveConfiguration.GetType()).FirstOrDefault();
                    if(driveConfigInfo == null)
                        throw new InvalidOperationException("The configuration is invalid");

                    xmlWriter.WriteStartElement(driveConfigInfo.Id);
                    drive.DriveConfiguration.Serialize(xmlWriter);
                    xmlWriter.WriteEndElement(); // ID
                }

                xmlWriter.WriteEndElement(); // VirtualDrive
            }
        }
        /// <summary>
        /// Serializes the device configuration to XML.
        /// </summary>
        /// <param name="xmlWriter">XmlWriter which will be used to write the configuration.</param>
        /// <param name="context">Current ConfigurationMapper instance.</param>
        private void SerializeDevices(XmlWriter xmlWriter, ConfigurationMapper context)
        {
            foreach(var device in this.Devices)
            {
                var deviceConfigInfo = context.DeviceConfigurations.Where(d => d.ConfigType == device.GetType()).FirstOrDefault();
                if(deviceConfigInfo == null)
                    throw new InvalidOperationException("The configuration is invalid.");

                xmlWriter.WriteStartElement(deviceConfigInfo.Id);
                device.Serialize(xmlWriter);
                xmlWriter.WriteEndElement(); // ID
            }
        }
        /// <summary>
        /// Deserializes configuration data from XML.
        /// </summary>
        /// <param name="xmlReader">XmlReader which will be used to read the configuration.</param>
        /// <param name="context">Current ConfigurationMapper instance.</param>
        private void Deserialize(XmlReader xmlReader, ConfigurationMapper context)
        {
            if(xmlReader.Name != "AeonVirtualMachineConfiguration")
                throw new InvalidDataException("The configuration file is invalid.");

            this.StartupPath = xmlReader.GetAttribute("StartupPath");
            this.Launch = xmlReader.GetAttribute("Launch");
            bool mouse;
            bool.TryParse(xmlReader.GetAttribute("IsMouseAbsolute") ?? string.Empty, out mouse);
            this.IsMouseAbsolute = mouse;

            int speed;
            if(!int.TryParse(xmlReader.GetAttribute("EmulationSpeed") ?? string.Empty, out speed))
                speed = 90;
            this.EmulationSpeed = speed;

            bool hideUI;
            bool.TryParse(xmlReader.GetAttribute("HideUserInterface") ?? string.Empty, out hideUI);
            this.HideUserInterface = hideUI;

            this.Title = xmlReader.GetAttribute("Title");

            xmlReader.Read();

            bool gotDrives = false;
            bool gotDevices = false;

            while(!gotDrives || !gotDevices)
            {
                if(xmlReader.Name == "VirtualDrives")
                {
                    if(gotDrives)
                        throw new InvalidDataException("The configuration file is invalid.");

                    foreach(var drive in DeserializeDrives(xmlReader, context))
                        this.Drives.Add(drive);

                    gotDrives = true;
                }
                else if(xmlReader.Name == "VirtualDevices")
                {
                    if(gotDevices)
                        throw new InvalidDataException("The configuration file is invalid.");

                    foreach(var device in DeserializeDevices(xmlReader, context))
                        this.Devices.Add(device);

                    gotDevices = true;
                }
                else
                    throw new InvalidDataException("The configuration file is invalid.");
            }
        }
        /// <summary>
        /// Deserializes drive configuration data from XML.
        /// </summary>
        /// <param name="xmlReader">XmlReader which will be used to read the configuration.</param>
        /// <param name="context">Current ConfigurationMapper instance.</param>
        private IEnumerable<MountedDrive> DeserializeDrives(XmlReader xmlReader, ConfigurationMapper context)
        {
            xmlReader.Read();
            while(xmlReader.Name == "VirtualDrive" && xmlReader.NodeType == XmlNodeType.Element)
            {
                var drive = new MountedDrive();

                var value = xmlReader.GetAttribute("DriveLetter");
                if(string.IsNullOrEmpty(value) || value.Length != 1 || !((value[0] >= 'A' && value[0] <= 'Z') || (value[0] >= 'a' && value[0] <= 'z')))
                    throw new InvalidDataException("The configuration file is invalid.");

                drive.DriveLetter = new DriveLetter(value[0]);
                try
                {
                    drive.DriveType = (Aeon.Emulator.DriveType)Enum.Parse(typeof(Aeon.Emulator.DriveType), xmlReader.GetAttribute("DriveType") ?? string.Empty, true);
                }
                catch(ArgumentException ex)
                {
                    throw new InvalidDataException("The configuration file is invalid.", ex);
                }

                xmlReader.Read();

                var configInfo = context.DriveConfigurations.Where(d => d.Id == xmlReader.Name).FirstOrDefault();
                if(configInfo == null)
                    throw new InvalidDataException("The configuration file is invalid.");

                var config = configInfo.CreateConfiguration();
                config.Deserialize(xmlReader);
                drive.DriveConfiguration = config;

                yield return drive;

                xmlReader.Read();
                if(xmlReader.Name == "VirtualDrive" && xmlReader.NodeType == XmlNodeType.EndElement)
                    xmlReader.Read();
            }

            if(xmlReader.Name != "VirtualDrives" || xmlReader.NodeType != XmlNodeType.EndElement)
                throw new InvalidDataException("the configuration file is invalid.");

            xmlReader.Read();
        }
        /// <summary>
        /// Deserializes device configuration data from XML.
        /// </summary>
        /// <param name="xmlReader">XmlReader which will be used to read the configuration.</param>
        /// <param name="context">Current ConfigurationMapper instance.</param>
        private IEnumerable<IDeviceConfiguration> DeserializeDevices(XmlReader xmlReader, ConfigurationMapper context)
        {
            xmlReader.Read();
            while(xmlReader.NodeType == XmlNodeType.Element)
            {
                var configInfo = context.DeviceConfigurations.Where(d => d.Id == xmlReader.Name).FirstOrDefault();
                if(configInfo == null)
                    throw new InvalidDataException("The configuration file is invalid.");

                var config = configInfo.Create();
                config.Deserialize(xmlReader);

                yield return config;

                xmlReader.Read();
            }

            xmlReader.Read();
        }
        /// <summary>
        /// Validates the StartupPath property value.
        /// </summary>
        private void ValidateStartupPath()
        {
            var path = this.StartupPath;
            if(string.IsNullOrEmpty(path))
            {
                SetPropertyError("StartupPath", "Startup path is required.");
                return;
            }

            var virtualPath = VirtualPath.TryParse(path);
            if(virtualPath == null)
            {
                SetPropertyError("StartupPath", "Invalid startup path.");
                return;
            }

            if(virtualPath.PathType == VirtualPathType.Relative || virtualPath.DriveLetter == null)
            {
                SetPropertyError("StartupPath", "Startup path must include a drive letter.");
                return;
            }

            if(this.Drives.AvailableDriveLetters.Contains((DriveLetter)virtualPath.DriveLetter))
            {
                SetPropertyError("StartupPath", "Invalid startup path.");
                return;
            }

            ClearPropertyError("StartupPath");
        }
        /// <summary>
        /// Validates the Launch property value.
        /// </summary>
        private void ValidateLaunch()
        {
            var launch = (this.Launch ?? string.Empty).Trim();
            if(string.IsNullOrEmpty(launch))
            {
                ClearPropertyError("Launch");
                return;
            }

            var target = launch.Split(new char[] { ' ', '\t' }, 1, StringSplitOptions.RemoveEmptyEntries);
            if(VirtualPath.TryParse(target[0]) == null)
            {
                SetPropertyError("Launch", "Invalid launch target.");
                return;
            }

            ClearPropertyError("Launch");
        }
        #endregion
    }
}
